<!doctype html>
<html lang="zh-CN">
<head>

    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    
    <meta name="referrer" content="no-referrer-when-downgrade">
    

    <title>《Flutter渲染原理与性能优化》 | 早起的年轻人</title>
    <meta property="og:title" content="《Flutter渲染原理与性能优化》 - 早起的年轻人">
    <meta property="og:type" content="article">
        
    <meta property="article:published_time" content='2021-07-31T21:39:34&#43;08:00'>
        
        
    <meta property="article:modified_time" content='2021-07-31T21:39:34&#43;08:00'>
        
    <meta name="Keywords" content="Flutter，golang,go语言,go语言笔记,飞雪无情,java,android,博客,项目管理,python,软件架构,公众号,小程序">
    <meta name="description" content="《Flutter渲染原理与性能优化》">
        
    <meta name="author" content="luckly">
    <meta property="og:url" content="https://luckly.work/post/Flutter%E6%B8%B2%E6%9F%93%E5%8E%9F%E7%90%86%E4%B8%8E%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/">
    <link rel="shortcut icon" href='/favicon.ico'  type="image/x-icon">

    <link rel="stylesheet" href='/css/normalize.css'>
    <link rel="stylesheet" href='/css/style.css'>
    <script type="text/javascript" src="//cdn.bootcdn.net/ajax/libs/jquery/3.4.1/jquery.min.js"></script>

    
    
    
        <link href="https://cdn.bootcdn.net/ajax/libs/fancybox/3.5.7/jquery.fancybox.min.css" rel="stylesheet">
    
    
    
    
        <link rel="stylesheet" href='/css/douban.css'>
    
        <link rel="stylesheet" href='/css/other.css'>
    
</head>

<link rel="stylesheet"
      href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.2.0/styles/default.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.2.0/highlight.min.js"></script>
<body>
    <header id="header" class="clearfix">
    <div class="container">
        <div class="col-group">
            <div class="site-name ">
                
                    <a id="logo" href="https://luckly.work/">
                        早起的年轻人
                    </a>
                
                <p class="description">专注于Flutter、Android、Java、Go语言(golang)、移动互联网、项目管理、软件架构</p>
            </div>
            <div>
                <nav id="nav-menu" class="clearfix">
                    <a class="current" href="https://luckly.work/">首页</a>
                    
                    <a  href="https://luckly.work/categories/" title="分类">分类</a>
                    
                    <a  href="https://luckly.work/tags/" title="标签">标签</a>
                    
                    <a  href="https://luckly.work/about/" title="关于我">关于我</a>
                    
                    <a  href="https://github.com/ITmxs/" title="github">github</a>
                    
                </nav>
            </div>
        </div>
    </div>
</header>

    <div id="body">
        <div class="container">
            <div class="col-group">

                <div class="col-8" id="main">
                    
<div class="res-cons">
    <style type="text/css">
    .post-toc {
        position: fixed;
        width: 200px;
        margin-left: -210px;
        padding: 5px 10px;
        font-family: Athelas, STHeiti, Microsoft Yahei, serif;
        font-size: 12px;
        border: 1px solid rgba(0, 0, 0, .07);
        border-radius: 5px;
        background-color: rgba(255, 255, 255, 0.98);
        background-clip: padding-box;
        -webkit-box-shadow: 1px 1px 2px rgba(0, 0, 0, .125);
        box-shadow: 1px 1px 2px rgba(0, 0, 0, .125);
        word-wrap: break-word;
        white-space: nowrap;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        z-index: 999;
        cursor: pointer;
        max-height: 70%;
        overflow-y: auto;
        overflow-x: hidden;
    }

    .post-toc .post-toc-title {
        width: 100%;
        margin: 0 auto;
        font-size: 20px;
        font-weight: 400;
        text-transform: uppercase;
        text-align: center;
    }

    .post-toc .post-toc-content {
        font-size: 15px;
    }

    .post-toc .post-toc-content>nav>ul {
        margin: 10px 0;
    }

    .post-toc .post-toc-content ul {
        padding-left: 20px;
        list-style: square;
        margin: 0.5em;
        line-height: 1.8em;
    }

    .post-toc .post-toc-content ul ul {
        padding-left: 15px;
        display: none;
    }

    @media print,
    screen and (max-width:1057px) {
        .post-toc {
            display: none;
        }
    }
</style>
<div class="post-toc" style="position: absolute; top: 188px;">
    <h2 class="post-toc-title">文章目录</h2>
    <div class="post-toc-content">
        <nav id="TableOfContents">
  <ul>
    <li><a href="#flutter示例说明">Flutter示例说明</a></li>
    <li><a href="#widge类型说明">Widge类型说明</a></li>
    <li><a href="#界面构建过程">界面构建过程</a>
      <ul>
        <li><a href="#widgetelement和renderobject三者关系">Widget、Element和RenderObject三者关系</a></li>
        <li><a href="#flutter与android界面开发对比">Flutter与Android界面开发对比</a></li>
        <li><a href="#界面创建过程">界面创建过程</a></li>
        <li><a href="#界面刷新过程">界面刷新过程</a></li>
      </ul>
    </li>
    <li><a href="#界面渲染过程">界面渲染过程</a>
      <ul>
        <li><a href="#约束条件">约束条件</a></li>
        <li><a href="#继续渲染过程分析">继续渲染过程分析</a></li>
      </ul>
    </li>
    <li><a href="#性能优化">性能优化</a></li>
    <li><a href="#flutter生命周期">Flutter生命周期</a>
      <ul>
        <li><a href="#state生命周期">State生命周期</a></li>
        <li><a href="#app生命周期">App生命周期</a></li>
        <li><a href="#widgetelement-与-renderobject生命周期">Widget、Element 与 RenderObject生命周期</a></li>
      </ul>
    </li>
  </ul>
</nav>
    </div>
</div>
<script type="text/javascript">
    $(document).ready(function () {
        var postToc = $(".post-toc");
        if (postToc.length) {
            var leftPos = $("#main").offset().left;
            if(leftPos<220){
                postToc.css({"width":leftPos-10,"margin-left":(0-leftPos)})
            }

            var t = postToc.offset().top - 20,
                a = {
                    start: {
                        position: "absolute",
                        top: t
                    },
                    process: {
                        position: "fixed",
                        top: 20
                    },
                };
            $(window).scroll(function () {
                var e = $(window).scrollTop();
                e < t ? postToc.css(a.start) : postToc.css(a.process)
            })
        }
    })
</script>
    <article class="post">
        <header>
            <h1 class="post-title">《Flutter渲染原理与性能优化》</h1>
        </header>
        <date class="post-meta meta-date">
            2021年7月31日
        </date>
        
        <div class="post-meta">
            <span>|</span>
            
            <span class="meta-category"><a href='/categories/flutter'>flutter</a></span>
            
        </div>
        
        
        <div class="post-meta">
            <span id="busuanzi_container_page_pv">|<span id="busuanzi_value_page_pv"></span><span>
                    阅读</span></span>
        </div>
        
        
        <div class="clear" style="display: none">
            <div class="toc-article">
                <div class="toc-title">文章目录</div>
            </div>
        </div>
        
        <div class="post-content">
            <blockquote>
<p>本文主要介绍</p>
</blockquote>
<p>之前的内容我们讲的是Flutter的基本布局控件使用方式，包括单元素布局与多元素布局。为什么会有这两种区分呢？还有一些控件根本没有布局方式，比如RichText，那它是怎么展示的？</p>
<p>这一节我们就来深入看下Flutter的Widget是怎么构建、布局、渲染的，它们之间又靠什么联系在一起。</p>
<p>首先我们从官方示例开始看起。</p>
<h2 id="flutter示例说明">Flutter示例说明</h2>
<p>使用Android Studio创建Flutter工程，会创建一个默认的界面，我们可以从这个界面分析出一点内容。</p>
<p><code>main.dart</code> 内容如下：</p>
<pre><code>import 'package:flutter/material.dart';

void main() =&gt; runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with &quot;flutter run&quot;. You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // &quot;hot reload&quot; (press &quot;r&quot; in the console where you ran &quot;flutter run&quot;,
        // or simply save your changes to &quot;hot reload&quot; in a Flutter IDE).
        // Notice that the counter didn't reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked &quot;final&quot;.

  final String title;

  @override
  _MyHomePageState createState() =&gt; _MyHomePageState();
}

class _MyHomePageState extends State&lt;MyHomePage&gt; {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      // This call to setState tells the Flutter framework that something has
      // changed in this State, which causes it to rerun the build method below
      // so that the display can reflect the updated values. If we changed
      // _counter without calling setState(), then the build method would not be
      // called again, and so nothing would appear to happen.
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          // Column is also layout widget. It takes a list of children and
          // arranges them vertically. By default, it sizes itself to fit its
          // children horizontally, and tries to be as tall as its parent.
          //
          // Invoke &quot;debug painting&quot; (press &quot;p&quot; in the console, choose the
          // &quot;Toggle Debug Paint&quot; action from the Flutter Inspector in Android
          // Studio, or the &quot;Toggle Debug Paint&quot; command in Visual Studio Code)
          // to see the wireframe for each widget.
          //
          // Column has various properties to control how it sizes itself and
          // how it positions its children. Here we use mainAxisAlignment to
          // center the children vertically; the main axis here is the vertical
          // axis because Columns are vertical (the cross axis would be
          // horizontal).
          mainAxisAlignment: MainAxisAlignment.center,
          children: &lt;Widget&gt;[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
</code></pre><p>之前写了很多常用widget的使用方法，但是还没有系统地对展示过程进行说明，这里对Demo中的每一行作用，进行详细说明：</p>
<ul>
<li><code>import 'package:flutter/material.dart';</code>这是dart语言的用法，这里导入的是MaterialDesign风格的包，里面有各种MaterialDesign风格控件以及常用控件。我们常用的MaterialApp、Scaffold、RaisedButton、Card等，都属于MaterialDesign风格的控件。</li>
<li><code>void main() =&gt; runApp(MyApp());</code> 这个是主方法入口，<code>main</code>方法为程序调用方法，<code>runApp</code>方法将一个Widget加到根节点中，添加方式稍后再原理解析中详细说明。<code>MyApp()</code>创建了一个Widget对象，下面的代码是MyApp类的声明。</li>
<li><code>MyApp</code>类声明中，说明了这是一个StatelessWidget的子类。也就是说，这个类应该是无状态的，里面的字段应该是不可变更的，为final类型。如果想要更新这个Widget的展示效果，只能重新创建一个新的对象来实现而不能复用原来对象。</li>
<li>MyApp的<code>build</code>方法返回了需要展示的Widget内容，这里返回的Widget和MyApp本身没有关系，只是他们的Element有关系，这一点要弄清楚，我之前也是在这一项卡了很久才理解。如果真的要想象这两个有关系，可以认为是父子关系。在这里返回的Widget是MaterialApp，这是一个典型的MaterialDesign应用，里面指定了theme，而在home中设置的是真正的界面widget.另外MaterialApp中也支持路由设置。</li>
<li><code>MyHomePage</code>类声明中，说明了这是一个StatefulWidget的子类，它与StatelessWidget的区别在于它无需重建类即可变更界面内容，变更方式通过State来实现。</li>
<li><code>_MyHomePageState</code>中存储了<code>MyHomePage</code>的状态信息，在State中进行变更，界面会随之变动。同样通过build方法提供界面展示效果，里面返回的是一个<code>Scaffold</code>类型Widget，这是最常用的MaterialDesign风格设计框架，提供了各种展示效果。在Demo中分别提供了AppBar、界面主体以及一个浮动按钮。在<code>_MyHomePageState</code>中还设置了一个方法，点击按钮时，更新界面展示内容。</li>
</ul>
<p>下面是一个这个界面的效果：</p>
<p>
        <a data-fancybox="gallery" href="https://luckly007.oss-cn-beijing.aliyuncs.com/img/image-20210731214258337.png">
            <img class="mx-auto" alt="image-20210731214258337" src="https://luckly007.oss-cn-beijing.aliyuncs.com/img/image-20210731214258337.png" />
        </a>
    </p>
<h2 id="widge类型说明">Widge类型说明</h2>
<p>在Demo中使用到了几种Widget？一一列出来：MyApp、MaterialApp、MyHomePage、Scaffold、AppBar、Text、Center、Column、FloatingActionButton、Icon.</p>
<p>这么多组件中，我们自己定义的只有两个：MyApp、MyHomePage，而这两个控件的父类，分别是：StatelessWidget、StatefulWidget，这两种也是Flutter推荐使用的两个父控件类型。</p>
<p>按照官方的设计原则，StatelessWidget、StatefulWidget这两种组合Widget基本上可以满足需要的所有情况，不过这不意味着我们不能自定义其它Widget.</p>
<p>下面列出几种经常遇到的基础Widget：</p>
<ul>
<li>StatelessWidget 组合型Widget，widget实例创建后，不可更改。比如：Container、RaisedButton等，如果有变动，需要重新创建实例。</li>
<li>StatefulWidget 组合型Widget，widget实例携带一个state实例，state实例内容可变更。这种Widget一般用于界面绘制的桥梁。但是如果滥用也会产生性能问题，本文后面会根据源码给出性能优化建议。</li>
<li>RenderObjectWidget 渲染型Widget，这种一般是基础widget，可以将该widget内容渲染出来。</li>
<li>SingleChildRenderObjectWidget 渲染型Widget，这种是RenderObjectWidget的一个子类，特点是该Widget只有一个子控件，比如之前介绍过的Padding、Align等。</li>
<li>MultiChildRenderObjectWidget 渲染型Widget，这种同样是RenderObjectWidget的一个子类，特点是该widget可以有超过一个的子控件，比如之前介绍过的Flex等。</li>
<li>LeafRenderObjectWidget 渲染型Widget，这种同样是RenderObjectWidget的一个子类，特点是该widget不会有子控件，只是由其本身进行渲染，比如RichText、RawImage等。</li>
<li>ProxyWidget 代理型Widget，这种widget用来进行数据在widget之间传递，比如常用的InheritedWidget，一般的状态管理框架也是基于这个原理实现的。</li>
</ul>
<p>我们的Demo，乃至于各种复杂的界面，主要也是这几种Widget组成的。</p>
<h2 id="界面构建过程">界面构建过程</h2>
<p>上面说了几种基础Widget类型，如果将Demo中涉及到的Widget整理成一个树（实际上widget不算是一个真正的树，至少不是一个静态树，真正的树是Element和RenderObject），可以看到如下结构：

        <a data-fancybox="gallery" href="https://luckly007.oss-cn-beijing.aliyuncs.com/img/Demo%E7%9A%84Widget%E6%A0%91.png">
            <img class="mx-auto" alt="Demo的Widget树" src="https://luckly007.oss-cn-beijing.aliyuncs.com/img/Demo%E7%9A%84Widget%E6%A0%91.png" />
        </a>
    </p>
<p>在树形结构中可以看到Widget的互相依赖过程，但是这个Widget树是怎样变成我们可见的界面的？中间经过了哪些转换过程？我们先说一下两个开发时没有涉及到的东西：<code>Element</code>和<code>RenderObject</code></p>
<ul>
<li>Element 这个是真正的节点，用来关联Widget与渲染对象。每一个Widget都对应着一个Element，但是Widget实例会经常变动，但是渲染树不能经常改变，因此Element会尽可能改变而不是重新创建。</li>
<li>RenderObject 是一个渲染节点数，这里面的每一个节点都会在界面上绘制出来。RenderObject与Element或者Widget不是一一对应的，只有RenderObjectWidget以及它的子类才会存在RenderObject</li>
</ul>
<h3 id="widgetelement和renderobject三者关系">Widget、Element和RenderObject三者关系</h3>
<p>上面简单介绍了Element、RenderObject，但是感觉还是不清楚，下面我们通过Flutter源码进行说明：</p>
<p>查看<code>Widget</code>类的代码，里面有这样一项：</p>
<pre><code>Element createElement();
</code></pre><p>这个就是Widget对应的Element，在不同的Widget中，会创建不同的Element，比如StatelessElement、StatefulElement、RenderObjectElement、SingleChildRenderObjectElement、MultiChildRenderObjectElement、LeafRenderObjectElement、ProxyElement等。因为是一一对应，同样在Element中也会持有Widget的对象：</p>
<pre><code>@override
  Widget get widget =&gt; _widget;
  Widget _widget;
</code></pre><p>而RenderObject则针对的是可渲染Widget，也就是RenderObjectWidget，部分代码如下：</p>
<pre><code>RenderObject createRenderObject(BuildContext context);
</code></pre><p>也就是说会在RenderObjectWidget的子类中创建RenderObject，但是实际上这样做只是为了方便开发人员使用，真正情况还是在Element中持有RenderObject对象，如下面代码所示：</p>
<pre><code>RenderObject get renderObject =&gt; _renderObject;
  RenderObject _renderObject;
  
void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _renderObject = widget.createRenderObject(this);
    ...
  }
</code></pre><p>所以三者的情况就是Widget用于开发人员设计控件，RenderObject用于界面渲染，Element连接两者展示。</p>
<h3 id="flutter与android界面开发对比">Flutter与Android界面开发对比</h3>
<p>Android界面开发是命令式的，如果需要变更某一个View，需要获取该View的句柄，然后对view进行参数变更。这样的实现方式有很强的针对性，同时绘制过程中也会自动判断，可以精准的对某一个View进行绘制。但是这种方式有一些缺点，如果变动的view比较多时，就需要为每一项单独设置，而且需要开发人员自己控制的话，较为复杂的界面逻辑需要很强的处理能力。</p>
<p>Flutter界面开发是声明式的，每次只要定义好数据项，同时声明这些数据项与Widget的绑定关系。真正使用时，只需要变更数据内容，然后重建Widget实例就可以了。这种方式的优势就是简单粗暴，并更数据集后，绑定的相关界面项会自动调整。但是所有相关Widget都换了一遍，如果渲染内容的实例（也就是RenderObject）也都重新变更一遍，那对于界面效果来说，是一个非常严重的打击。</p>
<p>但是实际上，Flutter的渲染效率很高，Widget虽然重建，但是Element以及RenderObject不一定会进行重建，具体的渲染过程，我们可以跟踪源码来看下。</p>
<h3 id="界面创建过程">界面创建过程</h3>
<p>main方法是主入口，里面只调用了runApp方法。可以看下这个方法的实现：</p>
<pre><code>void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..attachRootWidget(app)
    ..scheduleWarmUpFrame();
}
</code></pre><p>注意里面的<code>attachRootWidget</code>方法，这个方法是将我们提供的Widget添加到RootWidget中，具体代码如下：</p>
<pre><code>void attachRootWidget(Widget rootWidget) {
    _renderViewElement = RenderObjectToWidgetAdapter&lt;RenderBox&gt;(
      container: renderView,
      debugShortDescription: '[root]',
      child: rootWidget
    ).attachToRenderTree(buildOwner, renderViewElement);
  }
</code></pre><p>这里又调用了<code>attachToRenderTree</code>，这个方法将当前渲染内容加入到渲染树中，看相关代码：</p>
<pre><code>RenderObjectToWidgetElement&lt;T&gt; attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement&lt;T&gt; element]) {
    if (element == null) {
      owner.lockState(() {
        element = createElement();
        assert(element != null);
        element.assignOwner(owner);
      });
      owner.buildScope(element, () {
        element.mount(null, null);
      });
    } else {
      element._newWidget = this;
      element.markNeedsBuild();
    }
    return element;
  }
</code></pre><p>初始情况下，RootElement为空，因此创建新的Element。然后通过<code>BuildOwner.buildScope</code>将Element树以及RenderObject树添加子节点。实际上<code>buildScope</code>这个方法不仅在创建界面时用到，刷新界面时同样用到了，后面刷新界面时详细说明一下。</p>
<p>在创建界面时buildScope方法可以简化为：</p>
<pre><code>void buildScope(Element context, [VoidCallback callback]) {
    ···
        try {
          callback();
        } finally {
          ···
        }
      }
      ···
  }
</code></pre><p>由此可以看出，实际上只是执行了传入的callback方法。继续根据传入的方法，只是执行了如下语句：</p>
<pre><code>element.mount(null, null);
</code></pre><p>这个是根节点进行mount操作，因为没有父节点，所以parent传为空，查看相应代码：</p>
<pre><code>void mount(Element parent, dynamic newSlot) {
    assert(parent == null);
    super.mount(parent, newSlot);
    _rebuild();
  }
</code></pre><p><code>_rebuild()</code>代码如下：</p>
<pre><code>void _rebuild() {
    try {
      _child = updateChild(_child, widget.child, _rootChildSlot);
      assert(_child != null);
    } catch (exception, stack) {
      final FlutterErrorDetails details = FlutterErrorDetails(
        exception: exception,
        stack: stack,
        library: 'widgets library',
        context: 'attaching to the render tree'
      );
      FlutterError.reportError(details);
      final Widget error = ErrorWidget.builder(details);
      _child = updateChild(null, error, _rootChildSlot);
    }
  }
</code></pre><p>注意，<code>updateChild</code>同样也是界面创建于刷新时的重要处理过程，后面会详细说明，这里只需要认为这里会进行子控件的添加，而且是递归添加处理，分别调用子控件的<code>mount</code>操作。其中<code>widget.child</code>就是我们传入的Widget实例，在Demo中就是<code>MyApp()</code>。</p>
<p>查看<code>Element</code>类的mount方法，这里将RenderObject加入到相应的树中。</p>
<pre><code>void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _renderObject = widget.createRenderObject(this);
    assert(() { _debugUpdateRenderObjectOwner(); return true; }());
    assert(_slot == newSlot);
    attachRenderObject(newSlot);
    _dirty = false;
  }
  
void attachRenderObject(dynamic newSlot) {
    assert(_ancestorRenderObjectElement == null);
    _slot = newSlot;
    _ancestorRenderObjectElement = _findAncestorRenderObjectElement();
    _ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);
    final ParentDataElement&lt;RenderObjectWidget&gt; parentDataElement = _findAncestorParentDataElement();
    if (parentDataElement != null)
      _updateParentData(parentDataElement.widget);
  }
</code></pre><p>之前我们说过，Element与RenderObject不是一一对应的，所以需要寻找到可用的父RenderObject，再添加新的节点。</p>
<h3 id="界面刷新过程">界面刷新过程</h3>
<p>上面的创建过程很好理解，每个Widget都有一个Element，同时也与RenderObject保持关系。但是这样做很麻烦，为什么不直接创建一个渲染节点呢？就像Android那样做？还要维护三者间的关系。</p>
<p>我们之前也写过，Widget是可以随意创建的，但是Element却要尽可能地保持复用，所以刷新时这三者关系还要再好好设计。</p>
<p>界面刷新需要一个切入点(比如Android通过<code>invalidate</code>通知)，在Flutter中，就是通过State的<code>setState</code>方法来进行刷新。</p>
<p>查看setState源码，去掉一些无用代码</p>
<pre><code>void setState(VoidCallback fn) {
	···
    _element.markNeedsBuild();
  }
</code></pre><p>很简单，就是调用Element的<code>markNeedsBuild</code>方法，继续查看：</p>
<pre><code>void markNeedsBuild() {
    ···
    if (dirty)
      return;
    _dirty = true;
    owner.scheduleBuildFor(this);
  }
</code></pre><p>这里面将Element标记为dirty，然后调用<code>scheduleBuildFor</code>方法，继续查看：</p>
<pre><code>void scheduleBuildFor(Element element) {
    ···
    if (element._inDirtyList) {
      _dirtyElementsNeedsResorting = true;
      return;
    }
    if (!_scheduledFlushDirtyElements &amp;&amp; onBuildScheduled != null) {
      _scheduledFlushDirtyElements = true;
      onBuildScheduled();
    }
    _dirtyElements.add(element);
    element._inDirtyList = true;
    ···
  }
</code></pre><p>这里将该element加入到_dirtyElements中，标记这个节点刷新时需要进行处理。然后执行了<code>onBuildScheduled</code>方法。这个方法进行了什么操作，继续查找源码：</p>
<pre><code>buildOwner.onBuildScheduled = _handleBuildScheduled;

void _handleBuildScheduled() {
	···
	ensureVisualUpdate();
}

void ensureVisualUpdate() {
    switch (schedulerPhase) {
      case SchedulerPhase.idle:
      case SchedulerPhase.postFrameCallbacks:
        scheduleFrame();
        return;
      case SchedulerPhase.transientCallbacks:
      case SchedulerPhase.midFrameMicrotasks:
      case SchedulerPhase.persistentCallbacks:
        return;
    }
  }

void scheduleFrame() {
    ···
    window.scheduleFrame();
    _hasScheduledFrame = true;
  }
  
void scheduleFrame() native 'Window_scheduleFrame';
</code></pre><p>终于找到了，调用了一个native方法 <code>Window_scheduleFrame</code>，这个方法在Flutter Engine中实现。查看注释内容，会回调<code>onBeginFrame</code>和<code>onDrawFrame</code>这两个方法。继续查找源码：</p>
<pre><code>window.onBeginFrame = _handleBeginFrame;
window.onDrawFrame = _handleDrawFrame;
···
</code></pre><p>由于相关代码较多，这里简化一下：</p>
<pre><code>void drawFrame() {
    ···
    try {
      if (renderViewElement != null)
        buildOwner.buildScope(renderViewElement);
      super.drawFrame();
      buildOwner.finalizeTree();
    } finally {
      ···
    }
    ···
  }
</code></pre><p>又调用到了<code>buildOwner.buildScope</code>方法！</p>
<p>之前创建界面时采用了这个方法，现在刷新时也用到了，详细说明一下：</p>
<pre><code>void buildScope(Element context, [VoidCallback callback]) {
    if (callback == null &amp;&amp; _dirtyElements.isEmpty)
      return;
    ···
    Timeline.startSync('Build', arguments: timelineWhitelistArguments);
    try {
      _scheduledFlushDirtyElements = true;
      if (callback != null) {
        ···
        _dirtyElementsNeedsResorting = false;
        try {
          callback();
        } finally {
          ···
      	 }
      }
      _dirtyElements.sort(Element._sort);
      _dirtyElementsNeedsResorting = false;
      int dirtyCount = _dirtyElements.length;
      int index = 0;
      while (index &lt; dirtyCount) {
        ···
        try {
          _dirtyElements[index].rebuild();
        } catch (e, stack) {
          ···
        }
        index += 1;
        if (dirtyCount &lt; _dirtyElements.length || _dirtyElementsNeedsResorting) {
          _dirtyElements.sort(Element._sort);
          _dirtyElementsNeedsResorting = false;
          dirtyCount = _dirtyElements.length;
          while (index &gt; 0 &amp;&amp; _dirtyElements[index - 1].dirty) {
            // It is possible for previously dirty but inactive widgets to move right in the list.
            // We therefore have to move the index left in the list to account for this.
            // We don't know how many could have moved. However, we do know that the only possible
            // change to the list is that nodes that were previously to the left of the index have
            // now moved to be to the right of the right-most cleaned node, and we do know that
            // all the clean nodes were to the left of the index. So we move the index left
            // until just after the right-most clean node.
            index -= 1;
          }
        }
      }
      ···
        return true;
      }());
    } finally {
      for (Element element in _dirtyElements) {
        assert(element._inDirtyList);
        element._inDirtyList = false;
      }
      _dirtyElements.clear();
      _scheduledFlushDirtyElements = false;
      _dirtyElementsNeedsResorting = null;
      Timeline.finishSync();
      ···
      }
  }
</code></pre><p>代码中可以看到，首先将_dirtyElements进行排序，这是因为节点可能有很多个，如果其中两个节点存在级联关系，父级的Widget build操作必然会调用到子级的Widget build，如果子级又自己build一次，相当于出现了重复操作。因此通过深度排序就会避免这个问题。</p>
<p>排序结束后，对每一个Element进行遍历，执行rebuild操作。需要注意的是，如果在遍历过程中增加了新的节点，那么就需要重新排序。rebuild操作后面详细说明。</p>
<p>所有Element都rebuild后，清空_dirtyElements集合，节点状态恢复正常。</p>
<p>rebuild示意代码如下：</p>
<pre><code>void rebuild() {
    ···
    if (!_active || !_dirty)
      return;
    ···
    performRebuild();
    ···
  }
</code></pre><p>继续跟踪代码：</p>
<pre><code>void performRebuild() {
    ···
    Widget built;
    try {
      built = build();
      debugWidgetBuilderValue(widget, built);
    } catch (e, stack) {
      built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
    } finally {
      // We delay marking the element as clean until after calling build() so
      // that attempts to markNeedsBuild() during build() will be ignored.
      _dirty = false;
      ···
    }
    try {
      _child = updateChild(_child, built, slot);
      ···
    } catch (e, stack) {
      built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
      _child = updateChild(null, built, slot);
    }
    ···
  }
</code></pre><p>这里的<code>build</code>方法最终调用的是Widget中对应的build方法。<code>updateChild</code>方法同样也是创建界面时调用的方法，继续跟踪源码：</p>
<pre><code>Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    ···
    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }
    if (child != null) {
      if (child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        return child;
      }
      if (Widget.canUpdate(child.widget, newWidget)) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        ···
        return child;
      }
      deactivateChild(child);
      ···
    }
    return inflateWidget(newWidget, newSlot);
  }
</code></pre><p>这个方法就是Widget实例变更，但是Element实例不变的核心了，这里分成了四种情况分别处理：</p>
<ul>
<li>如果不存在新的Widget，那么说明这一个节点应该取消掉了，执行<code>deactivateChild</code>方法。</li>
<li>如果子节点的widget和新的widget一致（这里的一致指的是同一个对象，这个也是允许的），直接返回这个子节点。</li>
<li>如果两个widget不是同一个对象，判断类型是否相同，通过<code>canUpdate</code>方法判断，依据是Widget类型一致，同时Key一致。这种情况下，只需要更新子节点就好了。因此这一步就是widget变更，但是element不变更的原因。</li>
<li>其它情况下则认为子节点是新增的，调用<code>inflateWidget</code>进行子节点创建，里面与创建界面相同，执行了mount操作。</li>
</ul>
<p>上面的代码都是ComponentElement的类中处理方式，也就是常用的StatelessWidget与StatefulWidget使用的Element。这个过程比较复杂，按照我个人的见解来说，如果想要不进行变更，父级的Widget是不能改变的，否则无法找到锚点，所以界面刷新都是从StatefulWidget开始的，而不能从StatelessWidget开始。刷新开始后，在rebuild中进行递归处理，以StatefulWidget实例为锚点，一级一级地维护Widget与Element的关系。</p>
<p>如果不是ComponentElement，针对RenderObjectElement，则会调用下面的处理：</p>
<pre><code>void performRebuild() {
    widget.updateRenderObject(this, renderObject);
    _dirty = false;
  }
</code></pre><p>每一种RenderObjectElement都会有自己的<code>updateRenderObject</code>处理方式，类似于Android的View操作，针对每一个View来设置属性，这里不再详细说明。</p>
<p>到这里，<code>buildOwner.buildScope(renderViewElement);</code>方法就已经结束了，还记得不，再贴一下drawFrame代码：</p>
<pre><code>void drawFrame() {
    ···
    try {
      if (renderViewElement != null)
        buildOwner.buildScope(renderViewElement);
      super.drawFrame();
      buildOwner.finalizeTree();
    } finally {
      ···
    }
    ···
  }
</code></pre><p>剩下执行<code>super.drawFrame();</code>，通过pipelineOwner将RenderObject绘制到界面上，不再详细说说明：</p>
<pre><code>void drawFrame() {
    assert(renderView != null);
    pipelineOwner.flushLayout();
    pipelineOwner.flushCompositingBits();
    pipelineOwner.flushPaint();
    renderView.compositeFrame(); // this sends the bits to the GPU
    pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
  }
</code></pre><p>然后再执行<code>buildOwner.finalizeTree();</code>，这里面将一些设置为deactive的Element进行回收，这部分涉及到了生命周期，下面会详细说明。</p>
<h2 id="界面渲染过程">界面渲染过程</h2>
<p>继续上面的内容，前面说了界面构建的过程，创建好了RenderObject树，那么RenderObject tree怎么进行渲染呢？</p>
<p>继续看<code>drawFrame</code>方法：</p>
<pre><code>void drawFrame() {
    assert(renderView != null);
    pipelineOwner.flushLayout();
    pipelineOwner.flushCompositingBits();
    pipelineOwner.flushPaint();
    renderView.compositeFrame(); // this sends the bits to the GPU
    pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
  }
</code></pre><p><code>pipelineOwner.flushLayout()</code>对需要relayout的RenderObject对象重新测量。</p>
<pre><code>void flushLayout() {
    profile(() {
      Timeline.startSync('Layout', arguments: timelineWhitelistArguments);
    });
    ···
    try {
      // TODO(ianh): assert that we're not allowing previously dirty nodes to redirty themselves
      while (_nodesNeedingLayout.isNotEmpty) {
        final List&lt;RenderObject&gt; dirtyNodes = _nodesNeedingLayout;
        _nodesNeedingLayout = &lt;RenderObject&gt;[];
        for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) =&gt; a.depth - b.depth)) {
          if (node._needsLayout &amp;&amp; node.owner == this)
            node._layoutWithoutResize();
        }
      }
    } finally {
      ···
      profile(() {
        Timeline.finishSync();
      });
    }
  }
</code></pre><p>先将_nodesNeedingLayout集合根据节点深度进行排序，然后重新进行layout。_nodesNeedingLayout集合的内容在每个RenderObject更新时会进行标记的，比如RichText:</p>
<pre><code>set text(TextSpan value) {
    ···
    switch (_textPainter.text.compareTo(value)) {
      ···
      case RenderComparison.layout:
        _textPainter.text = value;
        _overflowShader = null;
        markNeedsLayout();
        break;
    }
  }
  
void markNeedsLayout() {
    ···
    owner._nodesNeedingLayout.add(this);
    owner.requestVisualUpdate();
  	···
  }
</code></pre><p>查看源码，<code>_layoutWithoutResize</code>方法基本上就是调用<code>performLayout</code>方法，这个方法在每个RenderObject中的实现都不一样，不过约定是进行布局展示后，调用child.layout方法，继续查看这个方法：</p>
<pre><code>void layout(Constraints constraints, { bool parentUsesSize = false }) {
    ···
    RenderObject relayoutBoundary;
    if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
      relayoutBoundary = this;
    } else {
      final RenderObject parent = this.parent;
      relayoutBoundary = parent._relayoutBoundary;
    }
    ···
    if (!_needsLayout &amp;&amp; constraints == _constraints &amp;&amp; relayoutBoundary == _relayoutBoundary) {
      ···
      return;
    }
    _constraints = constraints;
    _relayoutBoundary = relayoutBoundary;
    ···
    if (sizedByParent) {
      ···
      try {
        performResize();
        assert(() { debugAssertDoesMeetConstraints(); return true; }());
      } catch (e, stack) {
        _debugReportException('performResize', e, stack);
      }
      ···
    }
    RenderObject debugPreviousActiveLayout;
    ···
    try {
      performLayout();
      markNeedsSemanticsUpdate();
      assert(() { debugAssertDoesMeetConstraints(); return true; }());
    } catch (e, stack) {
      _debugReportException('performLayout', e, stack);
    }
    ···
    _needsLayout = false;
    markNeedsPaint();
  }
</code></pre><p>上面这段代码可以分析出很多东西：</p>
<p><code>!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject</code> 这四种判断条件设置relayoutBoundary为RenderObject本身，这有什么用？用处可大了。</p>
<p>约束条件是沿着树的深度，从上到下的，但是layout布局是从下到上的，这个很好理解。大部分情况下，父控件的大小除了约束条件外，还依赖于子控件的大小，所以要从下向上分别layout，当然paint正好相反，是从上到下的顺序，这个后面说明。relayoutBoundary指的是布局边界，也就是说，这个renderObject的大小是固定的，不会因为其子节点的大小而变化，这种情况下就可以认为这个RenderObject就是一个锚点，它的子节点有变动，不会影响到父节点的layout。</p>
<p>那上面这四种情况分别是什么呢？首先还是需要先说明下约束条件</p>
<h3 id="约束条件">约束条件</h3>
<p>为了简单说明，这里只说下<code>BoxConstraints</code>，也就是边界约束，而更为复杂的<code>SliverConstraints</code>其它文章中再详细说明。</p>
<p>先看下BoxConstraints的构造方法，里面就已经包括了所有属性：</p>
<pre><code>const BoxConstraints({
    this.minWidth = 0.0,
    this.maxWidth = double.infinity,
    this.minHeight = 0.0,
    this.maxHeight = double.infinity
  });
</code></pre><p>这个很好理解，看字面意思就能明白，分别约束了最大/最小宽度、最大/最小高度，也就是要求渲染后的视图一定要在这个范围内展示。而上面的构造方法则展示了最为宽松的约束条件：最小高度或宽度为0，最大高度或宽度为无穷大。</p>
<p>在实际使用时，一般会将父节点的约束条件传递给子节点，如果子节点有额外的约束条件，则进行比对添加，然后再传给下一级。</p>
<p>约束条件很简单，但是根据这四种条件，会有几种类型：</p>
<ul>
<li>tight 如果最小约束(minWidth，minHeight)和最大约束(maxWidth，maxHeight)是一样的，那么就限定死了这个节点的宽度与高度。</li>
<li>loose 如果最小约束都是0.0</li>
<li>bounded 如果最大约束都不是double.infinity</li>
<li>unbounded 如果最大约束都是double.infinity</li>
<li>expanding 如果最小约束和最大约束都是infinite</li>
</ul>
<p>明确约束的概念后，我们继续分析渲染过程。</p>
<h3 id="继续渲染过程分析">继续渲染过程分析</h3>
<p>之前说到了四种情况，下面分别进行说明：</p>
<ul>
<li>!parentUsesSize parentUsesSize表示父节点是否要依赖子节点的size，如果该值为false，子节点要重新布局的时候并不需要通知父节点</li>
<li>sizedByParent sizedByParent表示当前的节点虽然不是isTight，但是通过其他约束属性，也可以明确的知道size，比如Expanded，并不一定需要明确的size</li>
<li>constraints.isTight 这个上面已经说明了</li>
<li>parent is! RenderObject 这个更明确了，父节点都不能进行渲染，自然不能进行size操作</li>
</ul>
<p>非四种情况下，则调用<code>performResize</code>与<code>performLayout</code>遍历所有子节点，直到layout完成。</p>
<p>按照之drawFrame处理，<code>flushLayout</code>完成后进行<code>flushCompositingBits</code>，这个方法是用来为每个RenderObject设置适当needCompositing值，最终needCompositing将会决定生成多少layer提交给引擎，引擎中叠加绘制每一层layer（skia等经典用法，mix也是类似实现）。查看下面代码：</p>
<pre><code>void flushCompositingBits() {
    profile(() { Timeline.startSync('Compositing bits'); });
    _nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) =&gt; a.depth - b.depth);
    for (RenderObject node in _nodesNeedingCompositingBitsUpdate) {
      if (node._needsCompositingBitsUpdate &amp;&amp; node.owner == this)
        node._updateCompositingBits();
    }
    _nodesNeedingCompositingBitsUpdate.clear();
    profile(() { Timeline.finishSync(); });
  }

void _updateCompositingBits() {
    if (!_needsCompositingBitsUpdate)
      return;
    final bool oldNeedsCompositing = _needsCompositing;
    _needsCompositing = false;
    visitChildren((RenderObject child) {
      child._updateCompositingBits();
      if (child.needsCompositing)
        _needsCompositing = true;
    });
    if (isRepaintBoundary || alwaysNeedsCompositing)
      _needsCompositing = true;
    if (oldNeedsCompositing != _needsCompositing)
      markNeedsPaint();
    _needsCompositingBitsUpdate = false;
  }
</code></pre><p>每一个layer内容会同步变更，可以将一些paint较为复杂的节点单独设置一个layer，或者经常变动的paint设置为单独layer，这样可以减少多次paint导致的性能耗损。</p>
<p>继续查看<code>flushPaint</code>相关代码：</p>
<pre><code>void flushPaint() {
    profile(() { Timeline.startSync('Paint', arguments: timelineWhitelistArguments); });
    ···
    try {
      final List&lt;RenderObject&gt; dirtyNodes = _nodesNeedingPaint;
      _nodesNeedingPaint = &lt;RenderObject&gt;[];
      // Sort the dirty nodes in reverse order (deepest first).
      for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) =&gt; b.depth - a.depth)) {
        assert(node._layer != null);
        if (node._needsPaint &amp;&amp; node.owner == this) {
          if (node._layer.attached) {
            PaintingContext.repaintCompositedChild(node);
          } else {
            node._skippedPaintingOnLayer();
          }
        }
      }
      ···
    } finally {
      ···
      profile(() { Timeline.finishSync(); });
    }
  }
</code></pre><p>基本格式与前面一样，注意<code>PaintingContext.repaintCompositedChild</code>这个方法：</p>
<pre><code>static void _repaintCompositedChild(
    RenderObject child, {
    bool debugAlsoPaintedParent = false,
    PaintingContext childContext,
  }) {
    ···
    if (child._layer == null) {
      ···
      child._layer = OffsetLayer();
    } else {
      ···
      child._layer.removeAllChildren();
    }
    ···
    childContext ??= PaintingContext(child._layer, child.paintBounds);
    child._paintWithContext(childContext, Offset.zero);
    childContext.stopRecordingIfNeeded();
  }

void _paintWithContext(PaintingContext context, Offset offset) {
    ···
    if (_needsLayout)
      return;
    ···
    RenderObject debugLastActivePaint;
    ···
    _needsPaint = false;
    try {
      paint(context, offset);
      ···
    }
    ···
  }
</code></pre><p>isRepaintBoundary为true的RenderObject会创建一个自己的layer，最终调用了<code>RenderObject.paint</code>方法。</p>
<p>Flutter会把所有的layer都加入到ui.SceneBuilder对象中。然后在<code>renderView.compositeFrame()</code>中 ui.SceneBuilder会构建出ui.Scene（场景），交给ui.window.render方法去做最后真实渲染，最终绘制过程在Flutter引擎中实现并展示。</p>
<pre><code>void compositeFrame() {
    Timeline.startSync('Compositing', arguments: timelineWhitelistArguments);
    try {
      final ui.SceneBuilder builder = ui.SceneBuilder();
      final ui.Scene scene = layer.buildScene(builder);
      if (automaticSystemUiAdjustment)
        _updateSystemChrome();
      _window.render(scene);
      scene.dispose();
      ···
    } finally {
      Timeline.finishSync();
    }
  }
</code></pre><p>到现在为止，界面构建过程与渲染过程都已经说明完，那我们跟踪源码的目的是什么呢？至少我们可以做到下面三点：</p>
<ul>
<li>可以对源码进行修改。目前的代码还不是很完善，对于一些特殊的要求，我们可以通过源码修改的方式来完成，而且根据实践，在Android Studio中修改后的源码可以直接参与编译，不需要任何过程。</li>
<li>了解渲染过程，同时也了解了Widget、Element、RenderObject的处理方式以及生命周期，可以自己实现自定义的控件效果。</li>
<li>了解整个渲染过程，出现问题后能够知道瓶颈在哪里，同时增加绘制效率，防止卡顿发生。</li>
</ul>
<h2 id="性能优化">性能优化</h2>
<p>上面主要分析的是ComponentWidget以及相关Element，那就先从这里的优化方向说起。</p>
<p>之前我写过，绘制的锚点在于StatefulWidget，这个Widget的实例是不变的，而State的build方法，会创建出大量新的widget。这些widget对象创建本身就有开销，再加上element的对比判断等等，所以在这方面，我们可以尽可能地提高效率。</p>
<ul>
<li>StatelessWidget本身是不可变的，我们使用的StatefulWidget类都应该先判断是否值得，如果可以的话，应该使用StatelessWidget进行替代。比如每个界面的框架类，如果是不变的，就应该使用StatelessWidget，只有需要变更项才会使用StatefulWidget。</li>
<li>更新的StatefulWidget设计时尽可能地独立开来，非耦合功能拆分展示。</li>
<li>因为每次StatefulWidget的变更都会影响到其下的所有子节点，如果只是有限的几个控件变更，可以将这几个控件单独封装为一个StatefulWidget，单独刷新这个Widget，避免其他控件更新影响效率。这个算是最小封装原则，如下图所示，WidgetA这个控件需要更新，则将其单独封装一个StatefulWidget：</li>
</ul>
<p>
        <a data-fancybox="gallery" href="https://luckly007.oss-cn-beijing.aliyuncs.com/img/image-20210731214059526.png">
            <img class="mx-auto" alt="image-20210731214059526" src="https://luckly007.oss-cn-beijing.aliyuncs.com/img/image-20210731214059526.png" />
        </a>
    </p>
<ul>
<li>Widget的构造方法以及build方法会经常调用，避免在其中执行太多操作，可以转移的操作放在其它地方执行</li>
<li>可以尝试将部分Widget实例保持不变，比如增加const修饰，或者StreamBuilder方式指定对象等等，但是这样操作需要当心，有可能会引入问题。</li>
<li>如果是自定义控件，采用<code>CustomMultiChildLayout</code>等方式自定义布局展示，可以考虑下使用relayoutBoundary方式，减少节点布局设置</li>
<li>绘制时，一些可能会占用较多资源的控件build操作，可以加到<code>RepaintBoundry</code>控件中，比如静态图片，比较复杂的图片设置为一个单独的layer，避免重复build，在GPU中也会存在缓存，减少开销。</li>
<li>不可见的控件，尽量不进行build操作</li>
<li>自定义控件，避免在绘制时进行创建对象操作，尽可能复用配置，这个和Android是一样的。</li>
<li>尽量减少saveLayer操作，如果是透明效果或者裁剪效果，尽量设置到子控件上。</li>
</ul>
<p>除了与界面渲染相关的优化建议，实际上还有一切其它的性能优化项，比如：</p>
<ul>
<li>部分内容考虑延迟加载</li>
<li>较为耗时的计算操作放置到新的isolate中执行（isolate、Runner与event loop中异步处理的会单独说明，这些还是有很大区别的）</li>
<li>内存加载以及内存泄漏等进行优化</li>
</ul>
<h2 id="flutter生命周期">Flutter生命周期</h2>
<h3 id="state生命周期">State生命周期</h3>
<p>现在使用最多的就是StatefulWidget，先说下State的生命周期。查看State的方法，有这几个需要关注的（按照源码中查找的顺序）：initState、didUpdateWidget、reassemble、setState、deactivate、dispose、build、didChangeDependencies。</p>
<p>其中reassemble是为了开发调试使用的，hot reload时调用该方法，Release版本下该方法不会被调用到，因此通常情况下无需重载该方法。setState与build方法之前已经说明过，不需要再次说明。</p>
<p>其余的几种方法的生命周期如下图：</p>
<p>
        <a data-fancybox="gallery" href="https://luckly007.oss-cn-beijing.aliyuncs.com/img/image-20210731214048547.png">
            <img class="mx-auto" alt="image-20210731214048547" src="https://luckly007.oss-cn-beijing.aliyuncs.com/img/image-20210731214048547.png" />
        </a>
    </p>
<p>这张图是网上找的，内容比较全面，不过关于deactivate部分还需要再调整下，下面具体来说每一个方法：</p>
<ul>
<li>构造方法不用详细说明，创建State实例后才会执行各种操作。</li>
<li>initState 这个方法只在 <code>void _firstBuild()</code>中调用到，而_firstBuild方法只会在Element的<code>mount</code>方法中调用到，因此<code>initState</code>只会在这个控件第一次创建时才会触发。</li>
<li>didChangeDependencies 这个方法有很多触发地方，首次同样也是在_firstBuild方法中，在initState方法执行后触发。除此以外，还会在<code>notifyDependent</code>方法中触发，而notifyDependent方法在<code>void notifyClients(InheritedWidget oldWidget)</code>方法中调用到，最后的方法是InheritedWidget参数变更时的触发方法（InheritedWidget的具体原理以及常用方式在以后会详细说明）。
所以总结一下，didChangeDependencies有两种执行时机：1、会在initState之后执行；2、会在依赖的InheritedWidget发生变化的时执行</li>
<li>didUpdateWidget 这个方法会在<code>StatefulElement.update(StatefulWidget newWidget)</code>中执行，而后面一个方法我们很熟悉了，就是之前判断Widget变更的四种条件之一了，再看下源码：</li>
</ul>
<pre><code>if (Widget.canUpdate(child.widget, newWidget)) {
	if (child.slot != newSlot)
	  updateSlotForChild(child, newSlot);
	child.update(newWidget);
	return child;
}
</code></pre><p>所以didUpdateWidget的执行时机在于Widget实例变更了，但是Element实例没有变更的情况，也就是runtimeType与Key一致的情况。</p>
<ul>
<li>deactivate 调用时机同样在widget变更的四种条件之一，节点树构建时，发现某个节点不存在了，将其设置状态为deactvate。实际上deactivate调用后并不一定会直接调用dispose方法，framework在某些情况下会将remove掉的子树重新设置到其他位置，这时候会调用deactivate以及build方法，但不会调用dispose方法。例如路由设置，A界面跳转到B界面，这时A界面就会触发deactivate以及build方法。同样的如果从B界面返回到A界面，framework需要重新将子树放回放回原来的位置，同样会触发A界面的deactivate以及build方法。</li>
<li>dispose 这个方法就很明确了，当Element销毁时调用该方法，调用时机在<code>unmount</code>中。</li>
</ul>
<h3 id="app生命周期">App生命周期</h3>
<p>App生命周期监听可以通过<code>WidgetsBindingObserver</code>类进行设置，里面存在一个<code>void didChangeAppLifecycleState(AppLifecycleState state)</code>方法，实际使用时可以通过mixin方式进行监听。例如：</p>
<pre><code>class _MyHomePageState extends State&lt;MyHomePage&gt; with WidgetsBindingObserver {
  
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    switch(state) {
      case AppLifecycleState.inactive:
        print('inactive state.');
        break;
      case AppLifecycleState.resumed:
        print('resumed state.');
        break;
      case AppLifecycleState.paused:
        print('paused state.');
        break;
      case AppLifecycleState.suspending:
        print('suspending state.');
        break;
      default:
        print('unknown state.');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}
</code></pre><p>这四种状态分别进行说明：</p>
<ul>
<li>resumed 与Android类似，表示界面可见，可响应事件</li>
<li>inactive 表示无法获取焦点，无法响应用户事件，但是会有drawFrame回调，比如弹出dialog情况</li>
<li>paused 应用挂起，这种情况下drawFrame回调也不会有，比如退到后台</li>
<li>suspending ios中没有该状态，pause之后的状态，应用停止。该状态不常用</li>
</ul>
<p>常见的状态切换：</p>
<p>应用退到后台：inactive -&gt; paused</p>
<p>应用后台转到前台： inactive -&gt; resumed</p>
<h3 id="widgetelement-与-renderobject生命周期">Widget、Element 与 RenderObject生命周期</h3>
<p>之前的源码分析中已经做了详细的说明，有空的时候画一张图补上吧，下面是找的一张网上图片：</p>
<p>
        <a data-fancybox="gallery" href="https://luckly007.oss-cn-beijing.aliyuncs.com/img/image-20210731214012378.png">
            <img class="mx-auto" alt="image-20210731214012378" src="https://luckly007.oss-cn-beijing.aliyuncs.com/img/image-20210731214012378.png" />
        </a>
    </p>
        </div>

        
<div class="post-archive">
    <ul class="post-copyright">
        <li><strong>原文作者：</strong><a rel="author" href="https://luckly.work/">luckly</a></li>
        <li style="word-break:break-all"><strong>原文链接：</strong><a href="https://luckly.work/post/Flutter%E6%B8%B2%E6%9F%93%E5%8E%9F%E7%90%86%E4%B8%8E%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/">https://luckly.work/post/Flutter%E6%B8%B2%E6%9F%93%E5%8E%9F%E7%90%86%E4%B8%8E%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/</a></li>
        <li><strong>版权声明：</strong>本作品采用<a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议</a>进行许可，非商业转载请注明出处（作者，原文链接），商业转载请联系作者获得授权。</li>
    </ul>
</div>
<br/>



        

<div class="post-archive">
    <h2>See Also</h2>
    <ul class="listing">
        
        <li><a href="/post/Flutter%E6%B5%8B%E8%AF%95%E8%84%9A%E6%9C%AC%E5%BC%80%E5%8F%91/">《Flutter测试脚本开发》</a></li>
        
        <li><a href="/post/Flutter-Engine%E6%BA%90%E7%A0%81%E8%B0%83%E8%AF%95/">《Flutter Engine源码调试》</a></li>
        
        <li><a href="/post/FutureBuilder%E4%B8%8EStreamBuilder/">《FutureBuilder与StreamBuilder》</a></li>
        
        <li><a href="/post/Flutter%E5%BA%94%E7%94%A8%E9%9B%86%E6%88%90%E6%B5%85%E6%9E%90/">《Flutter应用集成浅析》</a></li>
        
        <li><a href="/post/Flutter-SizedOverflowBox-%E7%AE%80%E4%BB%8B%E5%8F%8A%E6%A1%88%E4%BE%8B/">《Flutter SizedOverflowBox 简介及案例》</a></li>
        
    </ul>
</div>


        <div class="post-meta meta-tags">
            
            <ul class="clearfix">
                
                <li><a href='/tags/flutter'>flutter</a></li>
                
            </ul>
            
        </div>
    </article>
    
    

    
    
    <div class="post bg-white">
      <script src="https://utteranc.es/client.js"
            repo= "https://github.com/ITmxs/repo"
            issue-term="pathname"
            theme="github-light"
            crossorigin="anonymous"
            async>
      </script>
    </div>
    
</div>

                    <footer id="footer">
    <div>
        &copy; 2021 <a href="https://luckly.work/">早起的年轻人 By luckly</a>
        
        | <a rel="nofollow" target="_blank" href="http://beian.miit.gov.cn/">粤ICP备2021号-1</a>
        
    </div>
    <br />
    <div>
        <div class="github-badge">
            <a href="https://juejin.cn/user/3843548384077192" target="_black" rel="nofollow"><span class="badge-subject">Powered by</span><span class="badge-value bg-blue">掘金</span></a>
        </div>
        <div class="github-badge">
            <a href="https://space.bilibili.com/480883651" target="_black"><span class="badge-subject">Design by</span><span class="badge-value bg-brightgreen">早起的年轻人</span></a>
        </div>
        <div class="github-badge">
            <a href="https://cloud.tencent.com/developer/user/6702670" target="_black"><span class="badge-subject">Theme</span><span class="badge-value bg-yellowgreen">云社区</span></a>
        </div>
    </div>
</footer>


    
    <script type="text/javascript">
        window.MathJax = {
            tex2jax: {
                inlineMath: [['$', '$']],
                processEscapes: true
                }
            };
    </script>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML' async></script><script src="https://cdn.bootcdn.net/ajax/libs/fancybox/3.5.7/jquery.fancybox.min.js"></script>

<a id="rocket" href="#top"></a>
<script type="text/javascript" src='/js/totop.js?v=0.0.0' async=""></script>



    <script type="text/javascript" src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js" async></script>




    <script src='/js/douban.js'></script>
    <script src="/js/copy-to-clipboard.js"></script>

                </div>

                <div id="secondary">
    <section class="widget">
        <form id="search" action='https://luckly.work/search/' method="get" accept-charset="utf-8" target="_blank" _lpchecked="1">
      
      <input type="text" name="q" maxlength="20" placeholder="Search">
      <input type="hidden" name="sitesearch" value="https://luckly.work/">
      <button type="submit" class="submit icon-search"></button>
</form>
    </section>
    
    <section class="widget">
        <h3 class="widget-title">最近文章</h3>
<ul class="widget-list">
    
    <li>
        <a href="https://luckly.work/xxx/xxx.html" title="信息化和信息系统">信息化和信息系统</a>
    </li>
    
    <li>
        <a href="https://luckly.work/post/Flutter/%E4%BD%BF%E7%94%A8flutter%E5%88%9B%E5%BB%BA%E6%B8%85%E5%8D%95/" title="使用flutter创建清单">使用flutter创建清单</a>
    </li>
    
    <li>
        <a href="https://luckly.work/post/Flutter/%E4%BD%BF%E7%94%A8flutter%E5%88%9B%E5%BB%BA%E5%B8%A6%E6%9C%89%E5%8A%A8%E7%94%BB%E7%9A%84%E5%88%9B%E6%84%8F%E6%B8%85%E5%8D%95/" title="使用flutter创建带有动画的创意清单">使用flutter创建带有动画的创意清单</a>
    </li>
    
    <li>
        <a href="https://luckly.work/post/Flutter/%E4%BD%BF%E7%94%A8flutter%E5%88%9B%E5%BB%BA%E6%90%9C%E7%B4%A2%E6%A0%8F/" title="使用flutter创建搜索栏">使用flutter创建搜索栏</a>
    </li>
    
    <li>
        <a href="https://luckly.work/post/Flutter/%E4%BD%BF%E7%94%A8flutter%E5%88%9B%E5%BB%BA%E5%93%8D%E5%BA%94%E5%BC%8F%E4%B8%8B%E6%8B%89%E5%AF%BC%E8%88%AA%E6%A0%8F/" title="使用flutter创建响应式下拉导航栏">使用flutter创建响应式下拉导航栏</a>
    </li>
    
    <li>
        <a href="https://luckly.work/post/Flutter/Flutter%E8%87%AA%E5%AE%9A%E4%B9%89%E5%BC%B9%E5%87%BA%E5%8A%A8%E7%94%BB/" title="Flutter自定义弹出动画">Flutter自定义弹出动画</a>
    </li>
    
    <li>
        <a href="https://luckly.work/post/Flutter/Flutter_WEB/" title="Flutter_WEB">Flutter_WEB</a>
    </li>
    
    <li>
        <a href="https://luckly.work/post/Flutter/Flutter%E7%B3%BB%E5%88%97%E5%88%97%E8%A1%A8%E5%92%8C%E7%BD%91%E6%A0%BC/" title="Flutter系列列表和网格">Flutter系列列表和网格</a>
    </li>
    
    <li>
        <a href="https://luckly.work/post/Flutter/Flutter%E8%A7%86%E8%A7%89%E9%87%8D%E5%8F%A0%E6%8C%87%E5%8D%97paddingviewpadding%E5%92%8Cviewinsets/" title="Flutter视觉重叠指南padding，viewpadding和viewinsets">Flutter视觉重叠指南padding，viewpadding和viewinsets</a>
    </li>
    
    <li>
        <a href="https://luckly.work/post/Flutter/Flutter-ListView-%E5%92%8CScrollPhysics%E8%AF%A6%E7%BB%86%E4%BB%8B%E7%BB%8D/" title="Flutter ListView 和ScrollPhysics详细介绍">Flutter ListView 和ScrollPhysics详细介绍</a>
    </li>
    
</ul>
    </section>

    
<section class="widget">
    <h3 class="widget-title" style="color:red">福利派送</h3>
    <ul class="widget-list">
        
        <li>
            <a href="https://blog.csdn.net/qq_39132095?spm=1001.2101.3001.5343" title="我的CSDN" target="_blank" style="color:red">
                
                    <img src="https://luckly007.oss-cn-beijing.aliyuncs.com/img/flutter.png">
                
            </a>
        </li>
        
        <li>
            <a href="https://space.bilibili.com/480883651" title="我的哔哩哔哩" target="_blank" style="color:red">
                
                    <img src="https://luckly007.oss-cn-beijing.aliyuncs.com/img/flutter.png">
                
            </a>
        </li>
        
        <li>
            <a href="https://www.zhihu.com/people/yimi-yang-guang-96-65" title="我的知乎" target="_blank" style="color:red">
                
                    <img src="https://luckly007.oss-cn-beijing.aliyuncs.com/img/flutter.png">
                
            </a>
        </li>
        
    </ul>
</section>


    <section class="widget">
        <h3 class="widget-title"><a href='/categories/'>分类</a></h3>
<ul class="widget-list">
    
    <li><a href="https://luckly.work/categories/Go/">Go (1)</a></li>
    
    <li><a href="https://luckly.work/categories/IT/">IT (1)</a></li>
    
    <li><a href="https://luckly.work/categories/Mysql/">Mysql (1)</a></li>
    
    <li><a href="https://luckly.work/categories/Nginx/">Nginx (1)</a></li>
    
    <li><a href="https://luckly.work/categories/YouTube%E8%A7%86%E9%A2%91%E4%B8%8B%E8%BD%BD/">YouTube视频下载 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/android/">android (6)</a></li>
    
    <li><a href="https://luckly.work/categories/dart/">dart (8)</a></li>
    
    <li><a href="https://luckly.work/categories/Flutter/">Flutter (141)</a></li>
    
    <li><a href="https://luckly.work/categories/git/">git (3)</a></li>
    
    <li><a href="https://luckly.work/categories/Go/">Go (4)</a></li>
    
    <li><a href="https://luckly.work/categories/ios/">ios (1)</a></li>
    
    <li><a href="https://luckly.work/categories/linux/">linux (1)</a></li>
    
    <li><a href="https://luckly.work/categories/read/">读书笔记 (6)</a></li>
    
    <li><a href="https://luckly.work/categories/redis/">redis (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E4%B8%AA%E4%BA%BA%E8%B5%84%E6%96%99/">个人资料 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E4%B9%A6%E5%8D%95/">书单 (8)</a></li>
    
    <li><a href="https://luckly.work/categories/%E4%B9%A6%E8%AF%84/">书评 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E4%BF%A1%E6%81%AF%E7%B3%BB%E7%BB%9F%E7%AE%A1%E7%90%86%E5%B8%88/">信息系统管理师 (8)</a></li>
    
    <li><a href="https://luckly.work/categories/%E4%BF%A1%E6%81%AF%E7%B3%BB%E7%BB%9F%E9%A1%B9%E7%9B%AE%E7%AE%A1%E7%90%86%E5%B8%88/">信息系统项目管理师 (16)</a></li>
    
    <li><a href="https://luckly.work/categories/%E5%8E%9F%E5%88%99/">原则 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E5%8E%9F%E7%94%9F%E9%80%9A%E8%AE%AF/">原生通讯 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E5%9F%BA%E7%A1%80/">基础 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E5%A4%8D%E5%88%A9%E6%95%88%E5%BA%94/">复利效应 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E5%AE%89%E8%A3%85%E6%95%99%E7%A8%8B/">安装教程 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E5%B0%91%E6%9C%89%E4%BA%BA%E8%B5%B0%E7%9A%84%E8%B7%AF/">少有人走的路 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E5%BF%83%E8%AF%AD/">心语 (3)</a></li>
    
    <li><a href="https://luckly.work/categories/%E6%8F%92%E4%BB%B6/">插件 (2)</a></li>
    
    <li><a href="https://luckly.work/categories/%E6%95%99%E5%AD%A6/">教学 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E7%8E%8B%E9%98%B3%E6%98%8E/">王阳明 (3)</a></li>
    
    <li><a href="https://luckly.work/categories/%E7%94%B5%E5%AD%90%E4%B9%A6/">电子书 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E7%99%BB%E9%99%86%E8%A1%A8%E5%8D%95/">登陆表单 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E7%A8%BB%E7%9B%9B%E5%92%8C%E5%A4%AB/">稻盛和夫 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E7%A9%B7%E7%88%B8%E7%88%B8%E5%AF%8C%E7%88%B8%E7%88%B8/">穷爸爸富爸爸 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E7%B2%BE%E8%BF%9B/">精进 (3)</a></li>
    
    <li><a href="https://luckly.work/categories/%E7%BC%96%E7%A8%8B/">编程 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E8%B4%A2%E5%8A%A1%E8%87%AA%E7%94%B1%E4%B9%8B%E8%B7%AF/">财务自由之路 (2)</a></li>
    
    <li><a href="https://luckly.work/categories/%E8%B7%91%E6%AD%A5/">跑步 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E8%B7%AF%E7%94%B1%E4%BC%A0%E5%8F%82/">路由传参 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E8%B7%AF%E7%BA%BF/">路线 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E8%BD%AF%E4%BB%B6%E5%AE%9E%E6%96%BD/">软件实施 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E9%98%B3%E6%98%8E%E5%BF%83%E5%AD%A6/">阳明心学 (3)</a></li>
    
    <li><a href="https://luckly.work/categories/%E9%A1%B9%E7%9B%AE/">项目 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E9%AD%85%E5%8A%9B/">魅力 (1)</a></li>
    
</ul>
    </section>

    <section class="widget">
        <h3 class="widget-title"><a href='/tags/'>标签</a></h3>
<div class="tagcloud">
    
    <a href="https://luckly.work/tags/go/">go</a>
    
    <a href="https://luckly.work/tags/IT/">IT</a>
    
    <a href="https://luckly.work/tags/Mysql/">Mysql</a>
    
    <a href="https://luckly.work/tags/Nginx/">Nginx</a>
    
    <a href="https://luckly.work/tags/YouTube%E8%A7%86%E9%A2%91%E4%B8%8B%E8%BD%BD/">YouTube视频下载</a>
    
    <a href="https://luckly.work/tags/android/">android</a>
    
    <a href="https://luckly.work/tags/dart/">dart</a>
    
    <a href="https://luckly.work/tags/flutter/">flutter</a>
    
    <a href="https://luckly.work/tags/git/">git</a>
    
    <a href="https://luckly.work/tags/go/">go</a>
    
    <a href="https://luckly.work/tags/ios/">ios</a>
    
    <a href="https://luckly.work/tags/linux/">linux</a>
    
    <a href="https://luckly.work/tags/redis/">redis</a>
    
    <a href="https://luckly.work/tags/%E4%B8%AA%E4%BA%BA%E8%B5%84%E6%96%99/">个人资料</a>
    
    <a href="https://luckly.work/tags/%E4%B9%A6%E5%8D%95/">书单</a>
    
    <a href="https://luckly.work/tags/%E4%B9%A6%E8%AF%84/">书评</a>
    
    <a href="https://luckly.work/tags/%E4%BF%A1%E6%81%AF%E7%B3%BB%E7%BB%9F%E7%AE%A1%E7%90%86%E5%B8%88/">信息系统管理师</a>
    
    <a href="https://luckly.work/tags/%E4%BF%A1%E6%81%AF%E7%B3%BB%E7%BB%9F%E9%A1%B9%E7%9B%AE%E7%AE%A1%E7%90%86%E5%B8%88/">信息系统项目管理师</a>
    
    <a href="https://luckly.work/tags/%E5%85%A5%E9%97%A8/">入门</a>
    
    <a href="https://luckly.work/tags/%E5%8E%9F%E5%88%99/">原则</a>
    
    <a href="https://luckly.work/tags/%E5%8E%9F%E7%94%9F%E9%80%9A%E8%AE%AF/">原生通讯</a>
    
    <a href="https://luckly.work/tags/%E5%9F%BA%E7%A1%80/">基础</a>
    
    <a href="https://luckly.work/tags/%E5%A4%8D%E5%88%A9%E6%95%88%E5%BA%94/">复利效应</a>
    
    <a href="https://luckly.work/tags/%E5%AE%89%E8%A3%85%E6%95%99%E7%A8%8B/">安装教程</a>
    
    <a href="https://luckly.work/tags/%E5%B0%91%E6%9C%89%E4%BA%BA%E8%B5%B0%E7%9A%84%E8%B7%AF/">少有人走的路</a>
    
    <a href="https://luckly.work/tags/%E5%BF%83%E8%AF%AD/">心语</a>
    
    <a href="https://luckly.work/tags/%E6%8F%92%E4%BB%B6/">插件</a>
    
    <a href="https://luckly.work/tags/%E6%95%99%E5%AD%A6/">教学</a>
    
    <a href="https://luckly.work/tags/%E7%8E%8B%E9%98%B3%E6%98%8E/">王阳明</a>
    
    <a href="https://luckly.work/tags/%E7%94%B5%E5%AD%90%E4%B9%A6/">电子书</a>
    
    <a href="https://luckly.work/tags/%E7%99%BB%E9%99%86%E8%A1%A8%E5%8D%95/">登陆表单</a>
    
    <a href="https://luckly.work/tags/%E7%A8%BB%E7%9B%9B%E5%92%8C%E5%A4%AB/">稻盛和夫</a>
    
    <a href="https://luckly.work/tags/%E7%A9%B7%E7%88%B8%E7%88%B8%E5%AF%8C%E7%88%B8%E7%88%B8/">穷爸爸富爸爸</a>
    
    <a href="https://luckly.work/tags/%E7%B2%BE%E8%BF%9B/">精进</a>
    
    <a href="https://luckly.work/tags/%E7%BC%96%E7%A8%8B/">编程</a>
    
    <a href="https://luckly.work/tags/%E8%AF%97/">诗</a>
    
    <a href="https://luckly.work/tags/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/">读书笔记</a>
    
    <a href="https://luckly.work/tags/%E8%B4%A2%E5%8A%A1%E8%87%AA%E7%94%B1%E4%B9%8B%E8%B7%AF/">财务自由之路</a>
    
    <a href="https://luckly.work/tags/%E8%B7%91%E6%AD%A5/">跑步</a>
    
    <a href="https://luckly.work/tags/%E8%B7%AF%E7%94%B1%E4%BC%A0%E5%8F%82/">路由传参</a>
    
    <a href="https://luckly.work/tags/%E8%B7%AF%E7%BA%BF/">路线</a>
    
    <a href="https://luckly.work/tags/%E8%BD%AF%E4%BB%B6%E5%AE%9E%E6%96%BD/">软件实施</a>
    
    <a href="https://luckly.work/tags/%E9%80%9A%E8%AE%AF%E5%BD%95/">通讯录</a>
    
    <a href="https://luckly.work/tags/%E9%98%B3%E6%98%8E%E5%BF%83%E5%AD%A6/">阳明心学</a>
    
    <a href="https://luckly.work/tags/%E9%A1%B9%E7%9B%AE/">项目</a>
    
    <a href="https://luckly.work/tags/%E9%AD%85%E5%8A%9B/">魅力</a>
    
</div>
    </section>

    
<section class="widget">
    <h3 class="widget-title">友情链接</h3>
    <ul class="widget-list">
        
        <li>
            <a target="_blank" href="https://www.liwenzhou.com/" title="李文周">李文周的博客</a>
        </li>
        
        <li>
            <a target="_blank" href="http://www.xbzweb.com/" title="小包子的博客">小包子的博客</a>
        </li>
        
        <li>
            <a target="_blank" href="https://www.flysnow.org/" title="飞雪无情的博客">飞雪无情的博客</a>
        </li>
        
        <li>
            <a target="_blank" href="http://yuedu.baidu.com/ebook/14a722970740be1e640e9a3e" title="Android Gradle权威指南">Android Gradle权威指南</a>
        </li>
        
        <li>
            <a target="_blank" href="https://gesdh.cn/" title="小格子">格子导航</a>
        </li>
        
        <li>
            <a target="_blank" href="https://itachi.xyz/" title="阿林">itachi&#39;s Blog</a>
        </li>
        
    </ul>
</section>


    <section class="widget">
        <h3 class="widget-title">其它</h3>
        <ul class="widget-list">
            <li><a href="https://luckly.work/index.xml">文章 RSS</a></li>
        </ul>
    </section>
</div>
            </div>
        </div>
    </div>
</body>

</html>